]> dgit.raspbian.org Git - nodejs.git/commitdiff
http2: defer rst stream while in scope
authorMatteo Collina <hello@matteocollina.com>
Wed, 1 Jul 2026 07:43:01 +0000 (09:43 +0200)
committerBastien Roucariès <rouca@debian.org>
Sun, 16 Aug 2026 17:34:06 +0000 (19:34 +0200)
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: https://github.com/nodejs-private/node-private/pull/921
Refs: https://hackerone.com/reports/3833629
Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
CVE-ID: CVE-2026-56848
origin: https://github.com/nodejs/node/commit/daa6d25e3dceb30edb832a778ec0610c8bc2dd12
bug: https://nodejs.org/en/blog/vulnerability/july-2026-security-releases#http2-re-entrant-send-can-cause-heap-use-after-free-cve-2026-56848---high

Gbp-Pq: Topic sec
Gbp-Pq: Name CVE-2026-56848.patch

src/node_http2.cc
test/parallel/test-http2-rst-stream-reentrancy.js [new file with mode: 0644]

index 2e7745f82f525593b738b7331575f454b99d55b7..f2f856b44f152176250562b5451d80b2b7592ead 100644 (file)
@@ -2334,10 +2334,18 @@ void Http2Stream::SubmitRstStream(const uint32_t code) {
   // if RST_STREAM received is not in scope and added to the list
   // causing endpoint to hang.
   if (session_->is_in_scope() && is_stream_cancel(code)) {
-      session_->AddPendingRstStream(id_);
-      return;
+    session_->AddPendingRstStream(id_);
+    return;
   }
 
+  // If RST_STREAM is submitted while nghttp2 is processing callbacks for
+  // a refused stream, don't force purge pending data. Sending pending data
+  // here can re-enter nghttp2 and close streams that are still being used
+  // by the active receive operation.
+  if (session_->is_in_scope() && code == NGHTTP2_REFUSED_STREAM) {
+    FlushRstStream();
+    return;
+  }
 
   // If possible, force a purge of any currently pending data here to make sure
   // it is sent before closing the stream. If it returns non-zero then we need
diff --git a/test/parallel/test-http2-rst-stream-reentrancy.js b/test/parallel/test-http2-rst-stream-reentrancy.js
new file mode 100644 (file)
index 0000000..8added7
--- /dev/null
@@ -0,0 +1,68 @@
+'use strict';
+const common = require('../common');
+if (!common.hasCrypto)
+  common.skip('missing crypto');
+
+const http2 = require('http2');
+const net = require('net');
+
+const PREFACE = Buffer.from('PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n');
+
+function frame(type, flags, sid, payload = Buffer.alloc(0)) {
+  const header = Buffer.alloc(9);
+  header.writeUIntBE(payload.length, 0, 3);
+  header[3] = type;
+  header[4] = flags;
+  header.writeUInt32BE(sid & 0x7fffffff, 5);
+  return Buffer.concat([header, payload]);
+}
+
+function goaway(lastStreamID, code) {
+  const payload = Buffer.alloc(8);
+  payload.writeUInt32BE(lastStreamID, 0);
+  payload.writeUInt32BE(code, 4);
+  return frame(7, 0, 0, payload);
+}
+
+function headers(sid) {
+  return frame(1, 0x05, sid, Buffer.from([
+    0x82,  // :method: GET
+    0x86,  // :scheme: http
+    0x84,  // :path: /
+    0x41, 0x01, 0x78,  // :authority: x
+  ]));
+}
+
+const server = http2.createServer();
+
+server.listen(0, common.mustCall(() => {
+  const socket = net.connect(server.address().port);
+  let sent = false;
+
+  socket.on('connect', common.mustCall(() => {
+    socket.write(Buffer.concat([PREFACE, frame(4, 0, 0)]));
+  }));
+
+  socket.on('error', () => {});
+
+  socket.on('data', common.mustCallAtLeast(() => {
+    if (sent)
+      return;
+    sent = true;
+
+    socket.write(frame(4, 1, 0));
+    socket.write(headers(1));
+
+    setImmediate(() => {
+      socket.write(Buffer.concat([
+        goaway(0, 0),
+        headers(3),
+        headers(5),
+        headers(7),
+      ]));
+      setTimeout(() => socket.destroy(), common.platformTimeout(50));
+    });
+  }));
+
+  socket.on('close', common.mustCall(() => server.close()));
+}));